
/* GCSx
** SAVE.CPP
**
** File saving and loading
*/

/*****************************************************************************
** Copyright (C) 2003-2006 Janson
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 of the License, or
** (at your option) any later version.
** 
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.
*****************************************************************************/

#include "all.h"

// Write interface

FileWrite::FileWrite(File* myFilePtr, int noCompression) throw_File { start_func
    filePtr = myFilePtr;
    zlibBufferOut = NULL;
    zlibStream = NULL;
    bytesWritten = 0;
    zlibStreamInit = 0;
    
    try {
        // @TODO: At some point we may intelligently determine whether to actually
        // use compression (and what level/type?) based on hints as to content
        // and size
        if (noCompression) {
            compression = WorldFileLoad::BLOCKCOMPRESSION_NONE;
        }
        else {
            compression = WorldFileLoad::BLOCKCOMPRESSION_ZLIB;
            // Save room to store original size
            seekBegin = filePtr->tell();
            filePtr->skip(4); // Exception point
            zlibBufferOut = new Byte[ZLIB_BUFFER_SIZE];
            zlibStream = new z_stream;
            // Use default alloc, free, and compression level for now
            zlibStream->zalloc = NULL;
            zlibStream->zfree = NULL;
            zlibStream->opaque = NULL;
            if (deflateInit(zlibStream, Z_DEFAULT_COMPRESSION) != Z_OK) {
                fatalCrash(0, "ZLIB Memory/compression error: %s", zlibStream->msg);
            }
            zlibStreamInit = 1;
            zlibStream->next_out = zlibBufferOut;
            zlibStream->avail_out = ZLIB_BUFFER_SIZE;
            bytesWritten = 4;
        }
    }
    catch (...) {
        if (zlibStreamInit) deflateEnd(zlibStream);
        delete zlibStream;
        delete[] zlibBufferOut;
        throw;
    }
}

FileWrite::~FileWrite() { start_func
    if (zlibStreamInit) deflateEnd(zlibStream);
    delete zlibStream;
    delete[] zlibBufferOut;
}

void FileWrite::flush() throw_File { start_func
    if (compression == WorldFileLoad::BLOCKCOMPRESSION_ZLIB) {
        zlibStream->next_in = NULL;
        zlibStream->avail_in = 0;
        int result;
        while ((result = deflate(zlibStream, Z_FINISH)) != Z_STREAM_END) {
            if (result == Z_STREAM_ERROR) {
                throw FileException("Compression error: %s", zlibStream->msg);
            }
            // Flush buffer
            assert(zlibStream->avail_out == 0);
            filePtr->write(zlibBufferOut, ZLIB_BUFFER_SIZE);
            zlibStream->next_out = zlibBufferOut;
            zlibStream->avail_out = ZLIB_BUFFER_SIZE;
        }
        // Final flush
        filePtr->write(zlibBufferOut, ZLIB_BUFFER_SIZE - zlibStream->avail_out);
        bytesWritten = 4 + zlibStream->total_out;
        zlibStreamInit = 0;
        if (deflateEnd(zlibStream) != Z_OK) {
            throw FileException("Compression error: %s", zlibStream->msg);
        }
        int totalWrote = zlibStream->total_in;
        delete zlibStream;
        zlibStream = NULL;
        // Return to beginning and write original size
        int position = filePtr->tell();
        filePtr->seek(seekBegin);
        filePtr->writeInt(totalWrote);
        filePtr->seek(position);
    }
}

void FileWrite::write(const void* ptr, int size) throw_File { start_func
    if (size) {
        if (compression == WorldFileLoad::BLOCKCOMPRESSION_ZLIB) {
            zlibStream->next_in = (Byte*)ptr;
            zlibStream->avail_in = size;
            // Still some to compress?
            while (zlibStream->avail_in) {
                // Need to flush?
                if (zlibStream->avail_out == 0) {
                    // Flush buffer 
                    filePtr->write(zlibBufferOut, ZLIB_BUFFER_SIZE);
                    zlibStream->next_out = zlibBufferOut;
                    zlibStream->avail_out = ZLIB_BUFFER_SIZE;
                }
                if (deflate(zlibStream, 0) == Z_STREAM_ERROR) {
                    throw FileException("Compression error: %s", zlibStream->msg);
                }
            }
            bytesWritten = 4 + zlibStream->total_out;
        }
        else {
            filePtr->write(ptr, size);
            bytesWritten += size;
        }
    }
}

void FileWrite::writeIntBulk(const Uint32* ptr, int count) throw_File { start_func
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
    write(ptr, count * 4);
#else
    for (int pos = 0; pos < count; ++pos) {
        writeInt(ptr[pos]);
    }
#endif
}

void FileWrite::writeInt(Uint32 value) throw_File { start_func
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
    write(&value, 4);
#else
    Uint8 bytes[4];
    bytes[0] = value & 0xFF;
    bytes[1] = (value >> 8) & 0xFF;
    bytes[2] = (value >> 16) & 0xFF;
    bytes[3] = (value >> 24) & 0xFF;
    write(bytes, 4);
#endif
}
    
void FileWrite::writeInt16(Uint16 value) throw_File { start_func
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
    write(&value, 2);
#else
    Uint8 bytes[2];
    bytes[0] = value & 0xFF;
    bytes[1] = (value >> 8) & 0xFF;
    write(bytes, 2);
#endif
}
    
void FileWrite::writeInt8(Uint8 value) throw_File { start_func
    write(&value, 1);
}
    
void FileWrite::writeStr(const string& str) throw_File { start_func
    writeInt16(str.size());
    write(str.c_str(), str.size());
}

// World file class

// (exception comes from WorldFileLoad)
WorldFile::WorldFile() throw_File : WorldFileLoad(), blockSaves() { start_func
    inSaving = 0;
    isModifiedAddDel = 0;
}

WorldFile::WorldFile(const string* openFile) throw_File : WorldFileLoad(openFile, File::FILE_MODE_READWRITE), blockSaves() { start_func
    emergencySave = NULL;
    inSaving = 0;
    isModifiedAddDel = 0;
    blockSaves.resize(numBlocks, NULL);
}

WorldFile::~WorldFile() { start_func
}
    
int WorldFile::isNew() { start_func
    return filename == NULL;
}
    
void WorldFile::claimBlock(Uint32 id, SaveLoad* claimingObject) { start_func
    WorldFileLoad::claimBlock(id, dynamic_cast<LoadOnly*>(claimingObject));
    blockSaves[id] = claimingObject;
}
    
void WorldFile::discardBlock(SaveLoad* claimingObject) { start_func
    assert(claimingObject);

    for (Uint32 pos = 0; pos < numBlocks; ++pos) {
        if (blockSaves[pos] == claimingObject) {
            blockSaves[pos] = NULL;
            blocks[pos].type = BLOCKTYPE_UNUSED;
            isModifiedAddDel = 1;
        }
    }
}

void WorldFile::newBlock(Uint32 blockType, SaveLoad* claimingObject) { start_func
    BlockInfo header;
    assert(blockType != BLOCKTYPE_UNUSED);
    
    // Setup header
    header.offset = 0;
    header.size = 0;
    header.type = blockType;
    header.object = claimingObject;
    header.header.version = 0;
    header.header.headerSize = 0;
    header.header.contentSize = 0;
    header.header.compression = BLOCKCOMPRESSION_NONE;
    blocks.push_back(header);
    
    blockSaves.push_back(claimingObject);
    
    ++numBlocks;
    isModifiedAddDel = 1;
}
    
void WorldFile::save() throw_File { start_func
    if (inSaving) return;

    assert(filePtr);
    assert(!isNew());
    // @TODO: determine if full save is more efficient or necessary
    // If not, only save blocks that are changed (requires blank spots to be
    // written in the headers)
    fullSave();
}

// Note that the uses of 'new File' are the main thing keeping us
// from saving in a memory exception or other emergency
void WorldFile::saveAs(const string* newFilename) throw_File { start_func
    if (inSaving) return;

    assert(newFilename);
    
    // Should mark ourselves as "in a save" so if we crash,
    // any safety "save all" doesn't try to save us, again
    inSaving = 1;

    int writingToTemp = 0;
    File* newFile = NULL;
    char* tempFilenameChar = NULL;

    // If we don't write to a tempfile, there'd be an error
    // if newfilename matches current filename
    // We only do this route if we appear to be writing to the
    // same filename and never if we don't already have a file open.
    if (newFilename == filename) writingToTemp = 1;
#if FILESYSTEM_CASE_SENSITIVE
    if ((filename) && (strcmp(newFilename->c_str(), filename->c_str()) == 0)) writingToTemp = 1;
#else
    if ((filename) && (myStricmp(newFilename->c_str(), filename->c_str()) == 0)) writingToTemp = 1;
#endif
    
    if (writingToTemp) {
        // New file- temp
        string tempPath;
        getPathname(newFilename->c_str(), tempPath);
        tempFilenameChar = tempnam(tempPath.c_str(), NULL);
        try {
            newFile = new File(tempFilenameChar, File::FILE_MODE_OVERWRITE);
        }
        catch (...) {
            free(tempFilenameChar);
            tempFilenameChar = NULL;
            throw;
        }
    }
    else {
        // New file- write directly
        newFile = new File(newFilename->c_str(), File::FILE_MODE_OVERWRITE);
    }
    
    Uint32 actualBlockCount = 0;

    try {
        // Count actual blocks used
        for (Uint32 pos = 0; pos < numBlocks; ++pos) {
            if ((blocks[pos].type != BLOCKTYPE_UNUSED) &&
                (blocks[pos].object != NULL)) {
                ++actualBlockCount;
            }
        }
        
        // Empty blocks
        actualBlockCount += NUM_EMPTY_BLOCK;
        
        // Header
        newFile->write(correctCookie, 4);
        newFile->writeInt(currentVersion[0]);
        newFile->writeInt(currentVersion[1]);
        newFile->writeInt(currentVersion[2]);
        newFile->writeInt(currentVersion[3]);
        newFile->writeBlanks(8); // Reserved
        newFile->writeInt(actualBlockCount);
        
        // Block infos- Where they'll go; reserve space
        int infoOffset = newFile->tell();
        newFile->writeBlanks(actualBlockCount * 16);
        
        // Write each block, and remember new details
        for (Uint32 pos = 0; pos < numBlocks; ++pos) {
            // Unused block? Will delete on final pass
            if ((blocks[pos].type == BLOCKTYPE_UNUSED) ||
                (!blocks[pos].object)) {
                blocks[pos].newOffset = 0;
                blocks[pos].newSize = 0;
            }
            else {
                blocks[pos].newOffset = newFile->tell();

                // Block header
                newFile->writeBlanks(16);
                            
                // Tell block to write header
                FileWrite* write = new FileWrite(newFile, 1);
                blocks[pos].newHeader.version = blockSaves[pos]->saveHeader(write);
                write->flush();
                blocks[pos].newHeader.headerSize = write->getSize();
                delete write;
                
                // Is content modified?
                if (blockSaves[pos]->isContentModified()) {
                    assert(!blockSaves[pos]->isContentCached());
                    
                    FileWrite* write = new FileWrite(newFile);
                    blockSaves[pos]->saveContent(write);
                    write->flush();
                    blocks[pos].newHeader.contentSize = write->getSize();
                    blocks[pos].newHeader.compression = write->getCompression();
                    delete write;
                }
                else {
                    // Copy from previous version
                    // (assert header was read at some point)
                    assert(blocks[pos].header.version);
                    
                    filePtr->seek(blocks[pos].offset + 16 + blocks[pos].header.headerSize);
                    newFile->copy(filePtr, blocks[pos].header.contentSize);
                    blocks[pos].newHeader.contentSize = blocks[pos].header.contentSize;
                    blocks[pos].newHeader.compression = blocks[pos].header.compression;
                }

                blocks[pos].newSize = 16 + blocks[pos].newHeader.headerSize + blocks[pos].newHeader.contentSize;
                
                // Seek back to fill in header
                int prevPos = newFile->tell();
                newFile->seek(blocks[pos].newOffset);
                newFile->writeInt(blocks[pos].newHeader.version);
                newFile->writeInt(blocks[pos].newHeader.headerSize);
                newFile->writeInt(blocks[pos].newHeader.contentSize);
                newFile->writeInt(blocks[pos].newHeader.compression);
                newFile->seek(prevPos);
            }
        }
       
        // Seek back to fill in info blocks
        newFile->seek(infoOffset);
        for (Uint32 pos = 0; pos < numBlocks; ++pos) {
            // Make sure a used block
            if ((blocks[pos].type != BLOCKTYPE_UNUSED) &&
                (blocks[pos].object != NULL)) {
                newFile->writeInt(blocks[pos].newOffset);
                newFile->writeInt(blocks[pos].newSize);
                newFile->writeInt(blocks[pos].type);
                newFile->writeInt(0); // Reserved
            }
        }
        
        // Done!
        newFile->flush();
        
        // From this point forward, there should hopefully be no exceptions,
        // because we could potentially lose our saved file, and our blocks
        // point to invalid locations
        
        // This shouldn't happen unless writing to a temp file AND somehow the
        // deletion of the original file or renaming of the temp file fails,
        // which shouldn't, because we obviously had access to write to
        // the original file in the first place
        
        if (writingToTemp) {
            // Close old file, but don't delete; close temp file also
            delete filePtr;
            delete newFile;
            newFile = NULL;
            filePtr = NULL;
            // Attempt rename, delete first though- rename doesn't overwrite
            // on all systems
            remove(newFilename->c_str());
            if (rename(tempFilenameChar, newFilename->c_str())) {
                // Failed- Warn user, but we can't recover
                fatalCrash(0, "Unable to save to %s- your file was saved as %s- error %s", newFilename->c_str(), tempFilenameChar, strerror(errno));
            }
            // Rename/overwrite successful- open it
            try {
                filePtr = new File(newFilename->c_str(), File::FILE_MODE_READWRITE);
            }
            catch (...) {
                // File saved, but for some reason, we can't reopen it
                // This is now a fatal exception that we can't recover from
                fatalCrash(0, "Your file was saved, but unable to reopen %s: %s", newFilename->c_str(), filename->c_str(), strerror(errno));
            }
            filename = newFilename;
        }
        else {
            // Not temp- just move file "pointer" over
            delete filePtr;
            filePtr = newFile;
            filename = newFilename;
            newFile = NULL;
        }
        
        // Delete temp filename
        if (tempFilenameChar) {
            free(tempFilenameChar);
            tempFilenameChar = NULL;
        }
    }
    // If an exception, we unmark inSaving, we'll be allowed to resave later
    catch (BaseException& e) {
        delete newFile;
        if (tempFilenameChar) {
            free(tempFilenameChar);
            tempFilenameChar = NULL;
        }
        inSaving = 0;
        throw;
    }
        
    // Tell all blocks of their new cache location and modified
    // status; if any of this throws, we're still "insaving".
    Uint32 pos;
    for (pos = 0; pos < numBlocks; ) {
        // Unused block? Delete
        if ((blocks[pos].type == BLOCKTYPE_UNUSED) ||
            (!blocks[pos].object)) {
            for (Uint32 subpos = pos; subpos < (numBlocks - 1); ++subpos) {
                blocks[subpos] = blocks[subpos + 1];
                blockSaves[subpos] = blockSaves[subpos + 1];
            }
            blocks.pop_back();
            blockSaves.pop_back();
            --numBlocks;
        }
        else {
            blocks[pos].offset = blocks[pos].newOffset;
            blocks[pos].size = blocks[pos].newSize;
            blocks[pos].header = blocks[pos].newHeader;

            // If THIS throws a fileexception, it becomes fatal, as
            // we just saved it and that means there's something wrong
            try {
                // @TODO: memory leak if cachedContent excepts and doesn't free contentFile
                // (would be a good place for auto_ptr)
                FileRead* contentFile = new FileRead(filePtr,
                                                     blocks[pos].offset + blocks[pos].header.headerSize + 16,
                                                     blocks[pos].header.contentSize,
                                                     blocks[pos].header.version,
                                                     blocks[pos].header.compression);

                blockSaves[pos]->cachedContent(contentFile, 0);
                blockSaves[pos]->saveSuccess();
            }
            catch (FileException& e) {
                fatalCrash(0, "Your file was saved, but error rereading file- %s", e.details);
            }

            ++pos;
        }
    }
    isModifiedAddDel = 0;
    
    for (; pos < actualBlockCount; ++pos) {
        BlockInfo header;
        header.offset = 0;
        header.size = 0;
        header.type = BLOCKTYPE_UNUSED;
        header.object = NULL;
        header.header.version = 0;
        header.header.headerSize = 0;
        header.header.contentSize = 0;
        header.header.compression = BLOCKCOMPRESSION_NONE;
        blocks.push_back(header);
        // Size of blocks, blockSaves, and numBlocks must be kept syncronized
        blockSaves.push_back(NULL);
        ++numBlocks;
    }

    inSaving = 0;
}
    
void WorldFile::fullSave() throw_File { start_func
    if (inSaving) return;

    assert(filePtr);
    assert(!isNew());
    assert(filename);
    
    // Just a save-as the current filename
    saveAs(filename);
}
    
int WorldFile::isModified() { start_func
    for (Uint32 pos = 0; pos < numBlocks; ++pos) {
        if (blockSaves[pos]) {
            if (blockSaves[pos]->isHeaderModified()) return 1;
            if (blockSaves[pos]->isContentModified()) return 1;
        }
    }    
    
    return isModifiedAddDel;
}

